/* The file is part of the Waff web framework.
 *
 * Copyright (c) 2011, Grzegorz Skorupa
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditionsare met:
 *
 * - Redistributions of source code must retain the above copyright notice, this list of conditions
 *   and the following disclaimer.
 * - Redistributi ons in binary form must reproduce the above copyright notice, this list of conditions
 *   and the following disclaimer in the documentation and/or other materials provided with the distribution.
 * - Neither the name of the Grzegorz Skorupa nor the names of its contributors may be used to endorse or promote products
 *   derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
 * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package pl.org.waff;

import java.lang.reflect.*;
import java.io.IOException;
import javax.servlet.http.*;
import javax.servlet.ServletException;
import java.util.logging.Logger;
import java.util.logging.Level;
import java.lang.StringBuffer;
import pl.org.waff.repository.Comment;
import pl.org.waff.repository.Document;
import pl.org.waff.repository.Folder;
import pl.org.waff.repository.ContentCache;

public class WaffServlet extends HttpServlet {

    public final static String SESSION_SCOPE = "session";
    public final static String REQUEST_SCOPE = "request";
    public final static String PROPERTIES_FILE_NAME = "/WEB-INF/conf/waff.properties";
    public final static String SITEMAP_FILE_NAME = "/WEB-INF/conf/sitemap.conf";
    public final static String LOGIC_FILE_NAME = "/WEB-INF/conf/logic.properties";
    public final static String USERS_FILE_NAME = "/WEB-INF/conf/users.properties";
    public final static String SCOPE_KEY_PART = ".scope";
    public final static String DISPATCH_KEY_PART = ".dispatch";
    public final static String SETTER_METHOD_PART = "set";
    public final static String JSP_EXTENSION = ".jsp";
    public final static String DELETE_ACTION = "delete";
    public final static String NO_REEROR = "0";
    public final static String PATH_ATTRIBUTE_NAME = "wafforyginalpath";
    public final static String PATH_PREFIX_ATTRIBUTE_NAME = "rootfolderprefix";
    public final static String DEVICE_ATTRIBUTE_NAME = "mobiledevice";
    public final static String SWITCH_ATTRIBUTE_NAME = "multiswitch";
    private static java.util.Properties properties;
    private static Properties navigationProperties;
    private static java.util.Properties logicProperties;
    private static Properties usersProperties;
    public static String errArgumentName;
    private static String securityManagerClass;
    private static String siteNavigatorClass;
    private static String userClass;
    private static String pathProcessorClass;
    private static String documentRepositoryClass;
    private static String userManagerClass;
    private static final Logger log = Logger.getLogger(WaffServlet.class.getName());
    private static SecurityManager securityManager;
    private static PathProcessor pathProcessor;
    private static SiteNavigator siteNavigator;
    private static ContentCache documentRepository;
    private static UserManager userManager;
    private static boolean methodDeclarationRequired;
    private static boolean objectsAccessibleByDefault;
    private static String applicationNamePrefix;
    private static boolean initialized = false;

    /**
     * @return the userManager
     */
    public static UserManager getUserManager() {
        return userManager;
    }

    //TODO: multivalue request parameters - NOT PROCESSED
    //TODO: handle file attachments
    //TODO: pathAccessible can redirect to restricted.jsp but objectAccessible and methodAccessible not - should be configurable?
    public void init() throws ServletException {
        //log.log(Level.INFO, "WaffServlet init");
        properties = new java.util.Properties();
        logicProperties = new java.util.Properties();
        navigationProperties = new Properties();
        usersProperties = new Properties();
        java.io.InputStream is = getServletContext().getResourceAsStream(PROPERTIES_FILE_NAME);
        java.io.InputStream is2 = getServletContext().getResourceAsStream(SITEMAP_FILE_NAME);
        java.io.InputStream is3 = getServletContext().getResourceAsStream(LOGIC_FILE_NAME);
        java.io.InputStream is4 = getServletContext().getResourceAsStream(USERS_FILE_NAME);
        try {
            properties.load(is);
            navigationProperties.load(is2);
            logicProperties.load(is3);
            usersProperties.load(is4);
            //loggingLevel = properties.getProperty("logging.level", "FINEST");
            errArgumentName = properties.getProperty("result.argument.name", "resultObject");
            securityManagerClass = properties.getProperty("security.manager.class", "pl.org.waff.DefaultSecurityManager");
            userClass = properties.getProperty("waffuser.class", "pl.org.waff.WaffUser");
            pathProcessorClass = properties.getProperty("path.processor.class", "pl.org.waff.DefaultPathProcessor");
            siteNavigatorClass = properties.getProperty("site.navigator.class", "pl.org.waff.DefaultNavigator");
            documentRepositoryClass = properties.getProperty("repository.class", "pl.org.waff.repository.InMemoryContentCache");
            userManagerClass = properties.getProperty("user.manager.class", "pl.org.waff.DefaultUserManager");

            // read applicationNamePrefix from properties and normalize
            applicationNamePrefix = properties.getProperty("application.prefix");
            if (applicationNamePrefix == null || "/".equals(applicationNamePrefix)) {
                applicationNamePrefix = "";
            } else if (applicationNamePrefix.length() > 0 && !applicationNamePrefix.startsWith("/")) {
                applicationNamePrefix = "/" + applicationNamePrefix;
            }
            getServletContext().setAttribute("applicationprefix", applicationNamePrefix);

            methodDeclarationRequired = Boolean.parseBoolean(properties.getProperty("mapping.check"));
            objectsAccessibleByDefault = Boolean.parseBoolean(properties.getProperty("objects.accessible.bydefault"));
            securityManager = (SecurityManager) Class.forName(securityManagerClass).newInstance();
            securityManager.init(logicProperties,
                    properties.getProperty("mobiledevice.handling"),
                    properties.getProperty("standarddevice.jspprefix"),
                    properties.getProperty("mobiledevice.jspprefix"));
            siteNavigator = (DefaultNavigator) Class.forName(siteNavigatorClass).newInstance();
            siteNavigator.init(navigationProperties, applicationNamePrefix);
            getServletContext().setAttribute("sitenavigator", siteNavigator);
            pathProcessor = (PathProcessor) Class.forName(pathProcessorClass).newInstance();
            pathProcessor.init(properties);
            documentRepository = (ContentCache) Class.forName(documentRepositoryClass).newInstance();
            documentRepository.init();
            getServletContext().setAttribute("repository", documentRepository);
            userManager = (UserManager) Class.forName(userManagerClass).newInstance();
            getUserManager().init(userClass, applicationNamePrefix, usersProperties);
            initialized = true;
        } catch (ClassNotFoundException e) {
            log.severe(e.getMessage());
        } catch (IOException e) {
            log.severe(e.getMessage());
        } catch (Exception e) {
            log.severe(e.getMessage());
        }
    }

    public void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        StringBuffer result = new StringBuffer("{");
        if (!initialized) {
            try {
                appendResult(result, "100", "Waff is not initialized");
                storeResultObject(req, result);
                getServletContext().getRequestDispatcher("/WEB-INF/error.jsp").forward(req, resp);
                return;
            } catch (javax.servlet.ServletException e) {
                log.severe(e.getMessage());
            }
        }
        String path = req.getServletPath();
        String objectName = getObjectName(path);
        String methodName = getMethodName(path);
        boolean allowFormsGetMethod = true;
        if (null != path) {
            if ((null != objectName) && allowFormsGetMethod) {
                processRequest(objectName, methodName, req, resp);
            }
            path = pathProcessor.modifyPath(path, req);
            dispatchRequest(path, methodName, req, resp);
        } else {
            //TODO:?
        }
    }

    public void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        StringBuffer result = new StringBuffer("{");
        if (!initialized) {
            try {
                appendResult(result, "100", "Waff is not initialized");
                storeResultObject(req, result);
                getServletContext().getRequestDispatcher("/WEB-INF/error.jsp").forward(req, resp);
                return;
            } catch (javax.servlet.ServletException e) {
                log.severe(e.getMessage());
            }
        }
        String path = req.getServletPath();
        String objectName = getObjectName(path);
        String methodName = getMethodName(path);
        if (null != path) {
            if (null != objectName) {
                processRequest(objectName, methodName, req, resp);
            }
            path = pathProcessor.modifyPath(path, req);
            dispatchRequest(path, methodName, req, resp);
        }
    }

    private void processRequest(String objectName, String methodName, HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        //boolean accessibleByDefault=false;
        boolean ok = false;
        boolean objectIsNew = false;
        boolean requestMethodOK = true;
        String objScope;
        String fullObjectName = logicProperties.getProperty(objectName);
        StringBuffer result = new StringBuffer("{");

        checkUser(req, userClass, getUserManager());
        if (null == fullObjectName) {
            result = appendResult(result, "1", "object " + objectName + " is not defined");
        } else {
            if (!securityManager.objectAccessible(objectName, objectsAccessibleByDefault, req)) {
                result = appendResult(result, "2", "user has no rights to object: " + objectName);
            } else {
                ok = true;
            }
        }
        if (!ok) {
            storeResultObject(req, result);
            return;
        }
        objScope = logicProperties.getProperty(objectName + SCOPE_KEY_PART, REQUEST_SCOPE);

        // is accessing object methods allowef if request type == GET ?
        String getMethodFormAllowed = logicProperties.getProperty(objectName + ".forms.get.method.allowed");
        // TODO: is it needed?
        if (null != getMethodFormAllowed && "false".equalsIgnoreCase(getMethodFormAllowed)) {
            requestMethodOK = !req.getMethod().equalsIgnoreCase("get");
            if (!requestMethodOK) {
                result = appendResult(result, "1", "method not accessible");
                storeResultObject(req, result);
                return;
            }
        }
        // Get object
        Class[] oneParameterArray = {String.class};
        Object[] oneArgumentArray = {null};
        Object bean;
        if (SESSION_SCOPE.equalsIgnoreCase(objScope)) {
            bean = req.getSession().getAttribute(objectName);
        } else {
            bean = req.getAttribute(objectName);
        }
        if (null == bean) {
            objectIsNew = true;
            try {
                bean = Class.forName(fullObjectName).newInstance();
                //we try to initialize the bean using String parameter defined in the properties file
                try {
                    Method initMethod = bean.getClass().getMethod("init", oneParameterArray);
                    oneArgumentArray[0] = logicProperties.getProperty(objectName + ".config");
                    initMethod.invoke(bean, oneArgumentArray);
                } catch (NoSuchMethodException e) {
                    log.severe(e.getMessage());
                } catch (SecurityException e) {
                    log.severe(e.getMessage());
                } catch (IllegalAccessException e) {
                    log.severe(e.getMessage());
                } catch (IllegalArgumentException e) {
                    log.severe(e.getMessage());
                } catch (InvocationTargetException e) {
                    log.severe(e.getMessage());
                }
            } catch (Exception e) {
                log.severe(e.getMessage());
                result = appendResult(result, "1", "the class is not defined");
                storeResultObject(req, result);
                return;
            }
        }
        // count request parameters
        // add set prefix to each request parameter and try to launch method with a request parameter as an argument
        // if there is no such method - do nothing
        java.util.Enumeration en = req.getParameterNames();
        String p;
        String v;
        String m = "";
        //Class[] oneParameterArray = {String.class};
        Class[] noParametersArray = {};
        Method myMethod;
        Object[] noArgumentsArray = {};
        char[] ca;
        while (en.hasMoreElements()) {
            p = (String) en.nextElement();
            v = req.getParameter(p);
            ca = p.toCharArray();
            ca[0] = Character.toUpperCase(ca[0]);
            p = new String(ca);
            m = SETTER_METHOD_PART + p;
            if (!securityManager.objectMethodAccessible(objectName, m, !methodDeclarationRequired, req)) {
                //setResultObject(req, "3", "user has no rights to method " + m + " of object " + objectName);
            } else {
                try {
                    myMethod = bean.getClass().getMethod(m, oneParameterArray);
                    oneArgumentArray[0] = v;
                    myMethod.invoke(bean, oneArgumentArray);
                } catch (NoSuchMethodException e) {
                    log.severe(e.getMessage());
                } catch (SecurityException e) {
                    log.severe(e.getMessage());
                } catch (IllegalAccessException e) {
                    log.severe(e.getMessage());
                } catch (IllegalArgumentException e) {
                    log.severe(e.getMessage());
                } catch (InvocationTargetException e) {
                    log.severe(e.getMessage());
                }
            }
        }
        // finally - lounch a method without parameters which name is taken from request path
        // DELETE_ACTION is additionally used only for removing object from session or request
        if (DELETE_ACTION.equalsIgnoreCase(methodName)) {
            if (SESSION_SCOPE.equalsIgnoreCase(objScope)) {
                req.getSession().removeAttribute(objectName);
            } else {
                req.removeAttribute(objectName);
            }
        } else {
            if (methodName != null) {
                if (!securityManager.objectMethodAccessible(objectName, methodName, !methodDeclarationRequired, req)) {
                    result = appendResult(result, "3", "user has no rights to method " + methodName + " of object " + objectName);
                } else {
                    try {
                        myMethod = bean.getClass().getMethod(methodName, noParametersArray);
                        myMethod.invoke(bean, noArgumentsArray);
                        invokeListener(req, resp, logicProperties.getProperty(objectName + ".listener"), bean, objectName+":"+methodName);
                    } catch (NoSuchMethodException e) {
                        log.severe(e.getMessage());
                    } catch (SecurityException e) {
                        log.severe(e.getMessage());
                    } catch (IllegalAccessException e) {
                        log.severe(e.getMessage());
                    } catch (IllegalArgumentException e) {
                        log.severe(e.getMessage());
                    } catch (InvocationTargetException e) {
                        log.severe(e.getMessage());
                    }
                }
            }
            if (SESSION_SCOPE.equalsIgnoreCase(objScope)) {
                req.getSession().setAttribute(objectName, bean);
            } else {
                req.setAttribute(objectName, bean);
            }
        }

        // save result object in request
        if (ok) {
            result = appendResult(result, WaffServlet.NO_REEROR, "");
        }

        String generatedContent = "";
        String dispatchPath = logicProperties.getProperty(objectName + DISPATCH_KEY_PART);
        if ("self".equals(dispatchPath)) {
            try {
                myMethod = bean.getClass().getMethod("getOutputString", noParametersArray);
                generatedContent = (String) myMethod.invoke(bean, noArgumentsArray);
            } catch (NoSuchMethodException e) {
                log.severe(e.getMessage());
            } catch (SecurityException e) {
                log.severe(e.getMessage());
            } catch (IllegalAccessException e) {
                log.severe(e.getMessage());
            } catch (IllegalArgumentException e) {
                log.severe(e.getMessage());
            } catch (InvocationTargetException e) {
                log.log(Level.FINEST, e.getMessage());
            }
            resp.getOutputStream().print(generatedContent);
        }
        storeResultObject(req, result);
    }

    private void invokeListener(HttpServletRequest req, HttpServletResponse resp, String listenerName, Object bean, String command) {
        if (listenerName == null) {
            return;
        }
        if("waff".equals(listenerName)){
            actionPerformed(new Event(bean,command), req, resp);
            return;
        }
        if (!securityManager.objectAccessible(listenerName, objectsAccessibleByDefault, req)) {
            //result = appendResult(result, "2", "user has no rights to object: " + listenerName);
            return;
        }
        String objScope = logicProperties.getProperty(listenerName + SCOPE_KEY_PART, REQUEST_SCOPE);
        Object listener;
        if (SESSION_SCOPE.equalsIgnoreCase(objScope)) {
            listener = req.getSession().getAttribute(listenerName);
        } else {
            listener = req.getAttribute(listenerName);
        }
        Method myMethod;
        Class[] oneParameterArray = {Event.class};
        Object[] oneArgumentArray = {null};
        try {
            myMethod = listener.getClass().getMethod("actionPerformed", oneParameterArray);
            oneArgumentArray[0] = new Event(bean,command);
            myMethod.invoke(listener, oneArgumentArray);
        } catch (NoSuchMethodException e) {
            log.severe(e.getMessage());
        } catch (SecurityException e) {
            log.severe(e.getMessage());
        } catch (IllegalAccessException e) {
            log.severe(e.getMessage());
        } catch (IllegalArgumentException e) {
            log.severe(e.getMessage());
        } catch (InvocationTargetException e) {
            log.severe(e.getMessage());
        }
    }
    
    private void actionPerformed(Event event, HttpServletRequest request, HttpServletResponse response){
        try{
            Document doc=(Document)event.getSource();
            if(documentRepository.putDocument(doc.getFolder(), doc)==ContentCache.ERR_NO_ERROR){
                //TODO:?
                event.setState(Event.PROCESSED);
            }else{
                //TODO:?
                event.setState(Event.ERROR);
            }
        }catch(Exception e){
            log.fine(e.getMessage());
        }
        try{
            Folder doc=(Folder)event.getSource();
            documentRepository.addFolder(doc.getPath());
            event.setState(Event.PROCESSED);
        }catch(Exception e){
            log.fine(e.getMessage());
        }
        try{
           Comment com=(Comment)event.getSource();
            documentRepository.putComment(com);
            event.setState(Event.PROCESSED);
        }catch(Exception e){
            log.fine(e.getMessage());
        }
        return;
    }

    private String getObjectName(String path) {
        String objectName = null;
        try {
            objectName = path.substring(path.lastIndexOf('/') + 1, path.indexOf('.'));
        } catch (Exception e) {
            log.fine(e.getMessage());
        }
        return objectName;
    }

    private String getObjectPath(String path) {
        String objectPath = "";
        try {
            objectPath = path.substring(0, path.lastIndexOf('/'));
        } catch (Exception e) {
            log.fine(e.getMessage());
        }
        return objectPath;
    }

    public static String getCurrentPath(String pathString) {
        String currentPath = null;
        try {
            if (pathString == null || pathString.length() == 0) {
                return "/";
            } else {
                if (applicationNamePrefix == null || "/".equals(applicationNamePrefix)) {
                    currentPath = pathString.substring(pathString.indexOf("/"), pathString.lastIndexOf("/"));
                } else {
                    currentPath = pathString.substring(pathString.indexOf(applicationNamePrefix) + applicationNamePrefix.length(), pathString.lastIndexOf("/"));
                }
                if (currentPath == null || currentPath.isEmpty()) {
                    currentPath = "/";
                }
            }
        } catch (Exception e) {
            log.severe(e.getMessage());
        }
        return currentPath;
    }

    private String getMethodName(String path) {
        String methodName = null;
        int idx = path.indexOf('.');
        int idx2 = path.lastIndexOf('.');
        if (idx != idx2) {
            methodName = path.substring(idx + 1, idx2);
        }
        return methodName;
    }

    //TODO: pathAccessible
    private void dispatchRequest(String path, String methodName, HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        //TODO: if object class is not properly declared but .dispatch is ok, then we will not be redirected to error.jsp - CORRECT THIS!
        log.fine("PATH(servlet):" + path);
        String objectName = getObjectName(path);
        String objectPath = getObjectPath(path);
        String jspName = null;
        String newPath = null;
        String indexLocation = properties.getProperty("index.jsp", "/WEB-INF" + objectPath + "/index.jsp");
        StringBuffer result = new StringBuffer("{");
        if (securityManager.pathAccessible(objectPath, req, (SiteNavigator) getServletContext().getAttribute("sitenavigator"))) {
            if (null == objectName || "_".equals(objectName)) {
                /**
                 * For dynamic systems with navigation structure and page content stored in database
                 * you should define key for instance: index.jsp=/WEB-INF/index.jsp
                 * In such case all requests will be dispatched to one jsp file with the presentation logic inside.
                 */
                jspName = indexLocation;
            } else {
                if (methodName != null) {
                    newPath = logicProperties.getProperty(objectName + "." + methodName + DISPATCH_KEY_PART);
                }
                if (newPath == null) {
                    newPath = logicProperties.getProperty(objectName + DISPATCH_KEY_PART);
                }
                if (null == newPath) {
                    // default object name to jsp mapping
                    //jspName = path.substring(0, path.indexOf('.')) + JSP_EXTENSION;

                    // if object name is not declared in waff.properties
                    result = appendResult(result, "1", "no object declaration found: " + objectName);
                    jspName = "/WEB-INF/error.jsp";
                } else if ("*".equalsIgnoreCase(newPath)) {
                    // 
                    jspName = indexLocation;
                } else if ("self".equalsIgnoreCase(newPath)) {
                    // if object generates response content
                    jspName = null;
                } else {
                    // if resulting jsp is declared in property file
                    jspName = newPath;
                }
            }
        } else {
            result = appendResult(result, "1", "access denied: " + objectPath);
            jspName = "/WEB-INF/restricted.jsp";
        }
        storeResultObject(req, result);
        if (null != jspName) {
            try {
                log.fine(jspName);
                getServletContext().getRequestDispatcher(jspName).forward(req, resp);
            } catch (javax.servlet.ServletException e) {
                log.severe(e.getMessage());
            }
        } else {
            //TODO: not implemented
            //resp.getOutputStream().print("hello!");
        }
    }

    public void checkUser(HttpServletRequest req, String userClass, UserManager userManager) {
        User user;
        user = userManager.getCurrentUser(req);
        req.getSession(true).setAttribute("waffuser", user);
    }

    private void storeResultObject(HttpServletRequest req, StringBuffer result) {
        result.append("}");
        req.setAttribute(errArgumentName, result.toString());
    }

    private StringBuffer appendResult(StringBuffer result, String code, String message) {
        if (result.length() == 0) {
            result.append("{");
        }
        result.append("\"").append(code).append("\":");
        result.append("\"").append(message).append("\":");
        return result;
    }

}
